/*
 * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the The BSD 3-Clause License
 * which accompanies this distribution, and is available at
 * http://opensource.org/licenses/BSD-3-Clause
 *
 * Contributors:
 *   Tada AB
 *   PostgreSQL Global Development Group
 *   Chapman Flack
 */
package org.postgresql.pljava.jdbc;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.ArrayList;

import org.postgresql.pljava.internal.ExecutionPlan;
import org.postgresql.pljava.internal.Portal;
import org.postgresql.pljava.internal.SPI;
import org.postgresql.pljava.internal.SPIException;

/**
 * Implementation of {@link Statement} for the SPI connection.
 * @author Thomas Hallgren
 */
public class SPIStatement implements Statement, SPIReadOnlyControl
{
	private final SPIConnection m_connection;
	
	// Default settings.
	//
	private String    m_cursorName     = null;
	private int       m_fetchSize      = 1000;
	private int       m_maxRows        = 0;
	private ResultSet m_resultSet      = null;
	private long      m_updateCount    = 0;
	private ArrayList<Object> m_batch  = null;
	private boolean   m_closed         = false;
	private short     m_readonly_spec  = ExecutionPlan.SPI_READONLY_DEFAULT;

	public SPIStatement(SPIConnection conn)
	{
		m_connection = conn;
	}

	public void addBatch(String statement)
	throws SQLException
	{
		// Statements are converted to native SQL once they
		// are executed.
		//
		this.internalAddBatch(statement);
	}

	public void cancel()
	throws SQLException
	{
	}

	public void clearBatch()
	throws SQLException
	{
		m_batch = null;
	}

	public void clearWarnings()
	throws SQLException
	{
	}

	private void clear()
	throws SQLException
	{
		if(m_resultSet != null)
			//
			// The close will call back to the resultSetClosed method
			// and set the m_resultSet to null.
			//
			m_resultSet.close();

		m_updateCount = -1;
		m_cursorName = null;
		m_batch = null;
	}

	public void close()
	throws SQLException
	{
		clear();
		m_closed = true;
	}

	public boolean execute(String statement)
	throws SQLException
	{
		// Ensure that the last statement is cleaned up
		// before we re-execute
		//
		this.clear();

		ExecutionPlan plan = ExecutionPlan.prepare(
			m_connection.nativeSQL(statement), null);

		int result = SPI.getResult();
		if(plan == null)
			throw new SPIException(result);

		try
		{
			return this.executePlan(plan, null);
		}
		finally
		{
			try { plan.close(); } catch(Exception e) {}
 		}
	}

	protected boolean executePlan(ExecutionPlan plan, Object[] paramValues)
	throws SQLException
	{
		m_updateCount = -1;
		m_resultSet   = null;

		boolean isResultSet = plan.isCursorPlan();
		if(isResultSet)
		{
			Portal portal = plan.cursorOpen(
				m_cursorName, paramValues, m_readonly_spec);
			m_resultSet = new SPIResultSet(this, portal, m_maxRows);
		}
		else
		{
			try
			{
				plan.execute(paramValues, m_readonly_spec, m_maxRows);
				m_updateCount = SPI.getProcessed();
			}
			finally
			{
				SPI.freeTupTable();
			}
		}
		return isResultSet;
	}

	/**
	 * Return of auto generated keys is not yet supported.
	 * @throws SQLException indicating that this feature is not supported.
	 */
	public boolean execute(String statement, int autoGeneratedKeys)
	throws SQLException
	{
		throw new UnsupportedFeatureException("Statement.execute(String,int)");
	}

	/**
	 * Return of auto generated keys is not yet supported.
	 * @throws SQLException indicating that this feature is not supported.
	 */
	public boolean execute(String statement, int[] columnIndexes)
	throws SQLException
	{
		throw new UnsupportedFeatureException("Statement.execute(String,int[])");
	}

	/**
	 * Return of auto generated keys is not yet supported.
	 * @throws SQLException indicating that this feature is not supported.
	 */
	public boolean execute(String statement, String[] columnNames)
	throws SQLException
	{
		throw new UnsupportedFeatureException("Statement.execute(String,String[])");
	}

	public int[] executeBatch()
	throws SQLException
	{
		int numBatches = (m_batch == null) ? 0 : m_batch.size();
		int[] result = new int[numBatches];
		for(int idx = 0; idx < numBatches; ++idx)
		{
			long count = this.executeBatchEntry(m_batch.get(idx));
			result[idx] = (count > Integer.MAX_VALUE)
				? SUCCESS_NO_INFO : (int)count;
		}
		return result;
	}

	public long[] executeLargeBatch()
	throws SQLException
	{
		int numBatches = (m_batch == null) ? 0 : m_batch.size();
		long[] result = new long[numBatches];
		for(int idx = 0; idx < numBatches; ++idx)
		{
			result[idx] = this.executeBatchEntry(m_batch.get(idx));
		}
		return result;
	}

	public ResultSet executeQuery(String statement)
	throws SQLException
	{
		this.execute(statement);
		return this.getResultSet();
	}

	public int executeUpdate(String statement)
	throws SQLException
	{
		this.execute(statement);
		return this.getUpdateCount();
	}

	/**
	 * Return of auto generated keys is not yet supported.
	 * @throws SQLException indicating that this feature is not supported.
	 */
	public int executeUpdate(String statement, int autoGeneratedKeys)
	throws SQLException
	{
		throw new UnsupportedFeatureException("Auto generated key support not yet implemented");
	}


	/**
	 * Return of auto generated keys is not yet supported.
	 * @throws SQLException indicating that this feature is not supported.
	 */
	public int executeUpdate(String statement, int[] columnIndexes)
	throws SQLException
	{
		throw new UnsupportedFeatureException("Auto generated key support not yet implemented");
	}


	/**
	 * Return of auto generated keys is not yet supported.
	 * @throws SQLException indicating that this feature is not supported.
	 */
	public int executeUpdate(String statement, String[] columnNames)
	throws SQLException
	{
		throw new UnsupportedFeatureException("Auto generated key support not yet implemented");
	}

	/**
	 * Returns the Connection from that created this statement.
	 * @throws SQLException if the statement is closed.
	 */
	public Connection getConnection()
	throws SQLException
	{
		if(m_connection == null)
			throw new StatementClosedException();
		return m_connection;
	}

	public int getFetchDirection()
	throws SQLException
	{
		return ResultSet.FETCH_FORWARD;
	}
	
	public int getFetchSize()
	throws SQLException
	{
		return m_fetchSize;
	}
	
	public ResultSet getGeneratedKeys()
	throws SQLException
	{
		throw new SQLException("JDK 1.4 functionality not yet implemented");
	}
	
	public int getMaxFieldSize()
	throws SQLException
	{
		return Integer.MAX_VALUE;
	}
	
	public int getMaxRows()
	throws SQLException
	{
		return m_maxRows;
	}
	
	public boolean getMoreResults()
	throws SQLException
	{
		return false;
	}
	
	public boolean getMoreResults(int current)
	throws SQLException
	{
		return false;
	}
	
	public int getQueryTimeout()
	throws SQLException
	{
		return 0;
	}
	
	public ResultSet getResultSet()
	throws SQLException
	{
		return m_resultSet;
	}

	public int getResultSetConcurrency()
	{
		return ResultSet.CONCUR_READ_ONLY;
	}

	public int getResultSetHoldability()
	throws SQLException
	{
		throw new SQLException("JDK 1.4 functionality not yet implemented");
	}

	public int getResultSetType()
	{
		return ResultSet.TYPE_FORWARD_ONLY;
	}

	public int getUpdateCount()
	throws SQLException
	{
		if ( m_updateCount > Integer.MAX_VALUE )
			throw new ArithmeticException(
				"too many rows updated to report in a Java signed int");
		return (int)m_updateCount;
	}

	public long getLargeUpdateCount()
	throws SQLException
	{
		return m_updateCount;
	}

	public SQLWarning getWarnings()
	throws SQLException
	{
		if (m_closed) {
			throw new SQLException("getWarnings: Statement is closed");
		}

		return null;
	}

	public void setCursorName(String cursorName)
	throws SQLException
	{
		m_cursorName = cursorName;
	}

	public void setEscapeProcessing(boolean enable)
	throws SQLException
	{
		throw new UnsupportedFeatureException("Statement.setEscapeProcessing");
	}


	/**
	 * Only {@link ResultSet#FETCH_FORWARD} is supported.
	 * @throws SQLException indicating that this feature is not supported
	 * for other values on <code>direction</code>.
	 */
	public void setFetchDirection(int direction)
	throws SQLException
	{
		if(direction != ResultSet.FETCH_FORWARD)
			throw new UnsupportedFeatureException("Non forward fetch direction");
	}

	public void setFetchSize(int size)
	throws SQLException
	{
		m_fetchSize = size;
	}

	public void setMaxFieldSize(int size)
	throws SQLException
	{
		throw new UnsupportedFeatureException("Statement.setMaxFieldSize");
	}

	public void setMaxRows(int rows)
	throws SQLException
	{
		m_maxRows = rows;
	}

	public void setQueryTimeout(int seconds)
	throws SQLException
	{
		throw new UnsupportedFeatureException("Statement.setQueryTimeout");
	}

	/**
	 * The argument is either a {@code String} containing SQL (if from a
	 * {@code Statement}, or an {@code Object} array of length three (if from
	 * a {@code PreparedStatement}) holding parameter values, SQL types, and
	 * PG type Oids.
	 */
	protected void internalAddBatch(Object batch)
	throws SQLException
	{
		if(m_batch == null)
			m_batch = new ArrayList<Object>();
		m_batch.add(batch);
	}

	protected long executeBatchEntry(Object batchEntry)
	throws SQLException
	{
		long ret = SUCCESS_NO_INFO;
		if(this.execute(m_connection.nativeSQL((String)batchEntry)))
			this.getResultSet().close();
		else if(m_updateCount >= 0)
			ret = m_updateCount;
		return ret;
	}

	void resultSetClosed(ResultSet rs)
	{
		if(rs == m_resultSet)
			m_resultSet = null;
	}

	// ************************************************************
	// Implementation of JDBC 4 methods. Methods go here if they
	// don't throw SQLFeatureNotSupportedException; they can be
	// considered implemented even if they do nothing useful, as
	// long as that's an allowed behavior by the JDBC spec.
	// ************************************************************

	public boolean isWrapperFor(Class<?> iface)
	throws SQLException
	{
	    return iface.isInstance(this);
	}

	public <T> T unwrap(Class<T> iface)
	throws SQLException
	{
	    if ( iface.isInstance(this) )
			return iface.cast(this);
		throw new SQLFeatureNotSupportedException
		( this.getClass().getSimpleName()
		  + " does not wrap " + iface.getName(),
		  "0A000" );
	}

	public boolean isCloseOnCompletion() throws SQLException
	{
		return false;
	}

	// ************************************************************
	// Non-implementation of JDBC 4 methods.
	// ************************************************************

	public void setPoolable(boolean poolable)
	throws SQLException
	{
	    throw new SQLFeatureNotSupportedException
		( this.getClass()
		  + ".setPoolable( boolean ) not implemented yet.",
		  "0A000" );
	}

	public boolean isPoolable()
	throws SQLException
	{
	    throw new SQLFeatureNotSupportedException
		( this.getClass()
		  + ".isPoolable() not implemented yet.",
		  "0A000" );
	}

	public boolean isClosed()
	throws SQLException
	{
	    throw new SQLFeatureNotSupportedException
		( this.getClass()
		  + ".isClosed() not implemented yet.",
		  "0A000" );
	}

	public void closeOnCompletion() throws SQLException
	{
	    throw new SQLFeatureNotSupportedException
		( this.getClass()
		  + ".closeOneCompletion() not implemented yet.",
		  "0A000" );
	}

	// ************************************************************
	// Implementation of the SPIReadOnlyControl extended interface
	// ************************************************************

	@Override
	public void defaultReadOnly()
	{
		m_readonly_spec = ExecutionPlan.SPI_READONLY_DEFAULT;
	}

	@Override
	public void forceReadOnly()
	{
		m_readonly_spec = ExecutionPlan.SPI_READONLY_FORCED;
	}

	@Override
	public void clearReadOnly()
	{
		m_readonly_spec = ExecutionPlan.SPI_READONLY_CLEARED;
	}
}

